#!/usr/bin/python

import os
import md5
import sys
import time
import socket
import irclib
import getopt
import logging
import logging.handlers
from datetime import datetime
import locale

# Models replicated because of TG's odd connection methods

from sqlobject import *

connection_string = "mysql://arkivo:arkivo@localhost/arkivo"
connection = connectionForURI(connection_string)
sqlhub.processConnection = connection

class Channel(SQLObject):
    class sqlmeta:
        orderBy = "name"
    name = UnicodeCol(length=60, alternateID=True)
    tracurl = UnicodeCol(length=200, default=None)
    topic = UnicodeCol(default=None)
    messages = MultipleJoin("Message")
    enabled = BoolCol()
    
    def __str__(self):
        return self.name
        
class Message(SQLObject):
    channel = ForeignKey("Channel")
    time = DateTimeCol()
    nick = UnicodeCol(length=30)
    line = UnicodeCol()
    isaction = BoolCol()
    
    def __str__(self):
        return "%s: %s" % (self.nick, self.line)


def become_daemon(ourHomeDir='.',outLog='/dev/null',errLog='/dev/null'):
    """
    Robustly turn us into a UNIX daemon, running in ourHomeDir.
    Modelled after the original code of this module and some
    sample code from the net.
    """

    # first fork
    try:
        if os.fork() > 0:
            sys.exit(0)     # kill off parent
    except OSError, e:
        sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
        sys.exit(1)
    os.setsid()
    os.chdir(ourHomeDir)
    os.umask(0)
    # second fork
    try:
        if os.fork() > 0:
            sys.exit(0)
    except OSError, e:
        sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
        sys.exit(1)

    si = open('/dev/null', 'r')
    so = open(outLog, 'a+', 0)
    se = open(errLog, 'a+', 0)
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())

host = None
port = 6667
nick = 'arkivobot'
secret = None
logfile = None
delay = 30
verbose = False
quiet = False
daemon = False
test = False

(opts, args) = getopt.getopt(sys.argv[1:], 'tvqbh:p:s:n:l:d:')

for o, v in opts:
    if o == '-h':
        host = v
    elif o == '-p':
        port = int(v)
    elif o == '-s':
        secret = v
    elif o == '-n':
        nick = v
    elif o == '-v':
        verbose = True
        quiet = False
    elif o == '-q':
        verbose = False
        quiet = True
    elif o == '-b':
        daemon = True
    elif o == '-l':
        logfile = v
    elif o == '-d':
        delay = int(v)
    elif o == '-t':
        test = True

if host is None:
    print "usage: loggerbot.py [-v] [-q] [-b] [-n <nick>] -h <host> [-p <port>] [-s <secret>] [-l <logfile>] [-d <onlinecheck-delay>]"
    sys.exit(1)

if daemon and logfile is None:
    logfile = os.path.expanduser('~/loggerbot.log')

if daemon: become_daemon()

fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdl = logging.StreamHandler()
if logfile is not None:
    hdl = logging.handlers.RotatingFileHandler(logfile, 'a', 1000000, 7)
hdl.setFormatter(fmt)
logging.getLogger().addHandler(hdl)
if quiet: logging.getLogger().setLevel(logging.WARN)
elif verbose: logging.getLogger().setLevel(logging.DEBUG)
else: logging.getLogger().setLevel(logging.INFO)

def refresh_channels():
    channels = []
    channelobjs = {}
    for channel in Channel.select():
        if channel.enabled:
            channels.append(channel.name)
            channelobjs[channel.name] = channel
    return channels, channelobjs

channels, channelobjs = refresh_channels()

class Client(irclib.SimpleIRCClient):

    def __init__(self, host, port, nick, password):
        irclib.SimpleIRCClient.__init__(self)
        self.inmotd = 0
        self.host = host
        self.port = port
        self.nick = nick
        self.password = password
        self.alternate = ''
        ctx = md5.new(password or '')
        ctx.update(host)
        ctx.update(str(time.time()))
        self.ticket = ctx.hexdigest()
        logging.info("TICKET %s" % self.ticket)
        self.last_check = 0
    
    def connect(self):
        try:
            irclib.SimpleIRCClient.connect(self, self.host, self.port, self.nick+self.alternate, self.password)
            return True
        except irclib.ServerConnectionError:
            logging.error('problems connecting to %s:%d', self.host, self.port)
            return False
 
    def check_nick(self, connect):
        if connect.get_nickname() != self.nick:
            connect.ison([self.nick])
    
    def join_channels(self, chans):
        global channels, channelobjs
        chandict = dict([(el, True) for el in chans])
        channels, channelobjs = refresh_channels()
        for c in chans:
            if c not in channelobjs:
                logging.info('parting removed channel %s', c)
                self.connection.part('#'+c)
        for c in channels:
            if c not in chandict:
                logging.info('joining missing channel %s', c)
                self.connection.join('#'+c)
                
        self.last_check = time.time()
        
    def on_topic(self, connect, event):
        logging.info("CHANGETOPIC %s :: %s" % (event.target(), ''.join(event.arguments())))
        chan = event.target().split('#')[1]
        chan = Channel.byName(chan)
        chan.topic = ''.join(event.arguments())
        
    def on_currenttopic(self, connect, event):
        chan = Channel.byName(event.arguments()[0].split('#')[1])
        chan.topic = event.arguments()[1]
        logging.info("CURRENTTOPIC %s :: %s" % (event.arguments()[0], event.arguments()[1]))

    def on_ison(self, connect, event):
        if self.nick not in [el.strip() for el in event.arguments()]:
            logging.warn('nick %s released, fetching it', self.nick)
            self.alternate = ''
            connect.nick(self.nick)
    
    def on_nick(self, connect, event):
        if event.target() == self.nick and connect.get_nickname() == self.nick:
            connect.privmsg('nickserv', 'identify %s' % self.password)
        self.check_nick(connect)

    def on_whoisuser(self, connect, event):
        if time.time() > self.last_check + 3*delay:
            logging.warn('not joined to any channel, rejoining them')
            self.join_channels([])

    def on_whoischannels(self, connect, event):
        chans = [el[1:].strip() for el in event.arguments()[1].split() if el.startswith('#')]
        self.join_channels(chans)
    
    def check_online(self):
        self.connection.privmsg(self.connection.get_nickname(), self.ticket)
        self.connection.whois([self.connection.get_nickname()])
        self.connection.execute_delayed(delay, self.check_online)
    
    def on_privmsg(self, connect, event):
        if ''.join(event.arguments()) == self.ticket:
            logging.debug('onlinecheck successfull')
        else:
            logging.warn('PRIVMSG %s', ''.join(event.arguments()))

    def on_motd(self, connect, event):
        if not self.inmotd:
            logging.info('MOTD started')
            self.inmotd = 1

    def on_endofmotd(self, connect, event):
        self.inmotd = 0
        logging.info('MOTD ended, join channels')
        connect.ison([self.nick])
        self.check_online()
    
    def on_nicknameinuse(self, connect, event):
        self.alternate += '_'
        connect.nick(self.nick+self.alternate)
    
    def on_join(self, connect, event):
        if event.source().split('!')[0] == self.nick+self.alternate:
            logging.info('JOINed %s', event.target())
    
    def on_privnotice(self, connect, event):
        logging.info('PRIVNOTICE %s', ''.join(event.arguments()))
    
    def on_pubmsg(self, connect, event):
        
        if event.target().startswith('#') and event.target()[1:] in channels:
            self.log_message(event.source().split('!')[0], event.target()[1:], ''.join(event.arguments()))
        self.check_nick(connect)
    
    def on_action(self, connect, event):
        if event.target().startswith('#') and event.target()[1:] in channels:
            self.log_message(event.source().split('!')[0], event.target()[1:], ''.join(event.arguments()), True)
        self.check_nick(connect)
    
    def log_message(self, source, target, message, isaction=False):
        if test: print target, source, message, isaction
        else:
            try:
                msg = Message(channel=channelobjs[target], nick=source, line=message, time=datetime.now(), isaction=isaction)
            except UnicodeDecodeError, e:
                logging.info('DECODEERROR %s' % e)
                d_source, enc = self.guess_encoding(source)
                logging.info('ENCODING source = %s' % enc)
                d_line, enc = self.guess_encoding(line)
                logging.info('ENCODING line = %s' % enc)
                msg = Message(channel=channelobjs[target], nick=d_source, line=d_line, time=datetime.now(), isaction=isaction)
            
    def guess_encoding(self, data):
        """
        Given a byte string, attempt to decode it.
        Tries the standard 'UTF8' and 'latin-1' encodings,
        Plus several gathered from locale information.

        The calling program *must* first call 
            locale.setlocale(locale.LC_ALL, '')

        If successful it returns 
            (decoded_unicode, successful_encoding)
        If unsuccessful it raises a ``UnicodeError``
        """
        successful_encoding = None
        # we make 'utf-8' the first encoding
        encodings = ['utf-8']
        #
        # next we add anything we can learn from the locale
        try:
            encodings.append(locale.nl_langinfo(locale.CODESET))
        except AttributeError:
            pass
        try:
            encodings.append(locale.getlocale()[1])
        except (AttributeError, IndexError):
            pass
        try:
            encodings.append(locale.getdefaultlocale()[1])
        except (AttributeError, IndexError):
            pass
        #
        # we try 'latin-1' last
        encodings.append('latin-1')
        for enc in encodings:
            # some of the locale calls 
            # may have returned None
            if not enc:
                continue
            try:
                decoded = unicode(data, enc)
                successful_encoding = enc

            except (UnicodeError, LookupError):
                pass
            else:
                break
        if not successful_encoding:
             raise UnicodeError(
            'Unable to decode input data.  Tried the following encodings: %s.'
            % ', '.join([repr(enc) for enc in encodings if enc]))
        else:
             return (decoded, successful_encoding)

socket.setdefaulttimeout(60)

irc = irclib.IRC()
server = Client(host, port, nick, secret)

# Set the locale (for guessing the encoding)
locale.setlocale(locale.LC_ALL, '')
try:
    while 1:
        if server.connect():
            logging.info('connecting to server')
            try: server.start()
            except irclib.ServerNotConnectedError: pass
        logging.info('dropped out of server loop')
        time.sleep(delay)
finally:
    logging.info('SYSTEM bot shutdown')
    logging.shutdown()

